All files / src/app/api/admin/components/[name]/restart route.ts

100% Statements 29/29
90.9% Branches 20/22
100% Functions 2/2
100% Lines 28/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101                        1x   1x                                         8x 8x 8x 1x   7x 1x     6x 6x 1x           5x 1x           4x 4x 4x 1x           3x     3x 3x 3x 3x 8x 2x 2x     3x   2x                 1x                  
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/lib/auth";
import {
  azureSubscriptionId,
  azureResourceGroup,
  isAzureDeployment,
  listContainerApps,
  restartContainerApp,
  setContainerAppMinReplicas,
} from "@/lib/azure-arm";
 
export const dynamic = "force-dynamic";
 
const ALLOWED_APPS = new Set([
  "mvhd-controlplane",
  "mvhd-dp-fhir",
  "mvhd-dp-omop",
  "mvhd-identityhub",
  "mvhd-issuerservice",
  "mvhd-keycloak",
  "mvhd-vault",
  "mvhd-tenant-mgr",
  "mvhd-provision-mgr",
  "mvhd-postgres",
  "mvhd-nats",
  "mvhd-neo4j",
  "mvhd-neo4j-proxy",
  "mvhd-ui",
]);
 
export async function POST(
  _req: NextRequest,
  { params }: { params: Promise<{ name: string }> },
) {
  const session = await getServerSession(authOptions);
  const roles = (session as { roles?: string[] } | null)?.roles ?? [];
  if (!session) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  if (!roles.includes("EDC_ADMIN")) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 });
  }
 
  const { name } = await params;
  if (!ALLOWED_APPS.has(name)) {
    return NextResponse.json(
      { error: `Unknown component: ${name}` },
      { status: 400 },
    );
  }
 
  if (!isAzureDeployment()) {
    return NextResponse.json(
      { error: "Restart is only available on Azure deployment" },
      { status: 400 },
    );
  }
 
  const sub = azureSubscriptionId();
  const rg = azureResourceGroup();
  if (!sub || !rg) {
    return NextResponse.json(
      { error: "Azure subscription / resource group not configured" },
      { status: 500 },
    );
  }
 
  try {
    // If currently scaled to zero, bump min-replicas to 1 first so restart
    // actually spins up a replica.
    let scaledUp = false;
    const apps = await listContainerApps(sub, rg);
    const app = apps.find((a) => a.name === name);
    const currentMin = app?.properties.template?.scale?.minReplicas ?? 1;
    if (currentMin === 0) {
      await setContainerAppMinReplicas(sub, rg, name, 1);
      scaledUp = true;
    }
 
    await restartContainerApp(sub, rg, name);
 
    return NextResponse.json({
      ok: true,
      name,
      scaledUp,
      message: scaledUp
        ? `${name}: bumped min-replicas 0→1 and restarted. Replica will boot shortly. Check diagnosis if it crashes.`
        : `${name}: restart triggered. Replicas will cycle within ~30s.`,
    });
  } catch (err) {
    return NextResponse.json(
      {
        error: "Restart failed",
        detail: err instanceof Error ? err.message : String(err),
      },
      { status: 502 },
    );
  }
}